package io.lvdaxian.mybatis06.datasource.pooled;

import io.lvdaxian.mybatis06.datasource.unpooled.UnpooledDataSource;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.*;
import java.util.logging.Logger;

/**
 * 其实 池化技术 是对 非池化的扩展
 *
 * @author lihh
 */
public class PooledDataSource implements DataSource {
  
  private final org.slf4j.Logger logger = LoggerFactory.getLogger(PooledDataSource.class);
  
  // 连接池 状态
  private final PoolState state = new PoolState(this);
  private final UnpooledDataSource dataSource;
  
  // 活跃连接数
  protected int poolMaximumActiveConnections = 10;
  // 空闲连接数
  protected int poolMaximumIdleConnections = 5;
  // 在被强制收回之前,池中连接被检查的时间
  protected int poolMaximumCheckoutTime = 20000;
  // 这是给连接池一个打印日志状态机会的低层次设置,还有重新尝试获得连接, 这些情况下往往需要很长时间 为了避免连接池没有配置时静默失败)。
  protected int poolTimeToWait = 20000;
  // 发送到数据的侦测查询,用来验证连接是否正常工作 主要是为了避免对不同数据库的特定实现进行硬编码，从而提高框架的通用性和可移植性
  // mybatis 源码中使用了 NO PING QUERY SET
  protected String poolPingQuery = "SELECT 1";
  // 开启或禁用侦测查询
  protected boolean poolPingEnabled = true;
  // 用来配置 poolPingQuery 多次时间被用一次
  protected int poolPingConnectionsNotUsedFor = 0;
  
  private int expectedConnectionTypeCode;
  
  public PooledDataSource() {
    this.dataSource = new UnpooledDataSource();
  }
  
  protected void pushConnection(PooledConnection connection) throws SQLException {
    synchronized (state) {
      state.activeConnections.remove(connection);
      
      // 判断连接是否有效
      if (connection.isValid()) {
        state.accumulatedCheckoutTime += connection.getCheckoutTime();
        
        // 它首先检查数据库连接是否处于自动提交模式，如果不是，则调用rollback()方法执行回滚操作。
        // 在MyBatis中，如果没有开启自动提交模式，则需要手动提交或回滚事务。因此，这段代码可能是在确保操作完成后，如果没有开启自动提交模式，则执行回滚操作。
        // 总的来说，这段代码用于保证数据库的一致性，确保操作完成后，如果未开启自动提交模式，则执行回滚操作。 （为了保证数据库 中的数据一致性）
        if (!connection.getRealConnection().getAutoCommit())
          connection.getRealConnection().rollback();
        
        // 如果空闲连接 小于设定的，那就是太小了
        if (state.idleConnections.size() < poolMaximumIdleConnections && connection.getConnectionTypeCode() == expectedConnectionTypeCode) {
          // 实例化一个新的DB连接，加入到idle列表
          PooledConnection newConnection = new PooledConnection(connection.getRealConnection(), this);
          state.idleConnections.add(newConnection);
          
          newConnection.setCreatedTimestamp(connection.getCreatedTimestamp());
          newConnection.setLastUsedTimestamp(connection.getLastUsedTimestamp());
          // 此时该连接放到连接池中了 处于一个无效的状态
          connection.invalidate();
          logger.info("Returned connection {} to pool.", newConnection.getRealHashCode());
          
          // 通知其他线程可以来抢DB连接了
          state.notifyAll();
          
          // 能执行到这里，说明空闲的连接是满的
        } else {
          // 将connection关闭  因为已经超过最大连接空闲数，所以没必要回收了，直接close掉
          connection.getRealConnection().close();
          logger.info("Closed connection {}.", connection.getRealHashCode());
          connection.invalidate();
        }
      } else {
        logger.info("A bad connection ({}) attempted to return to the pool, discarding connection.", connection.getRealHashCode());
        state.badConnectionCount++;
      }
    }
  }
  
  /**
   * 原则上 从连接池中弹出一个connection，但是如果连接池为空的化，新建一个直接返回
   *
   * @param username db username
   * @param password db password
   * @return 返回 PooledConnection
   * @author lihh
   */
  private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;
    
    while (null == conn) {
      // 锁膨胀
      synchronized (state) {
        // 首先 判断空闲连接有吗
        if (!state.idleConnections.isEmpty()) {
          conn = state.idleConnections.remove(0);
          logger.info("Checked out connection {} from pool.", conn.getRealHashCode());
          
          // 如果能执行到 else的情况，说明空闲连接为空的
        } else {
          // 活跃连接数不足
          if (state.activeConnections.size() < poolMaximumActiveConnections) {
            conn = new PooledConnection(dataSource.getConnection(), this);
            logger.info("Created connection {}.", conn.getRealHashCode());
            
            // 能执行到此处 说明活跃连接是满的
          } else {
            // 取得活跃链接列表的第一个，也就是最老的一个连接
            PooledConnection oldestActiveConnection = state.activeConnections.get(0);
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
            
            // 如果时间过长的话，标注为过期
            if (longestCheckoutTime > poolMaximumCheckoutTime) {
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
              // 此时 移除过期的连接
              state.activeConnections.remove(oldestActiveConnection);
              
              // 如果连接都过期了，为了保证数据的一致性，肯定是要回滚的
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                oldestActiveConnection.getRealConnection().rollback();
              }
              
              // 删掉最老的链接，然后重新实例化一个新的链接
              // 这里 其实复用了真实的连接
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
              oldestActiveConnection.invalidate();
              logger.info("Claimed overdue connection {}.", conn.getRealHashCode());
              
              // 如果checkout超时时间不够长，则等待
            } else {
              try {
                // 默认情况下 是false，意味着第一次就会进来
                if (!countedWait) {
                  state.hadToWaitCount++;
                  countedWait = true;
                }
                
                logger.info("Waiting as long as {} milliseconds for connection.", poolTimeToWait);
                long wt = System.currentTimeMillis();
                // 此时线程挂起，处于等待时间
                state.wait(poolTimeToWait);
                state.accumulatedWaitTime += System.currentTimeMillis() - wt;
              } catch (Exception e) {
                break;
              }
            }
          }
        }
      }
    }
    
    // 针对 连接 conn做有效的判断
    if (null != conn) {
      
      // 此时 conn 是一个新的连接，如果该新的连接还是有效的话，直接rollback
      if (conn.isValid()) {
        if (!conn.getRealConnection().getAutoCommit()) {
          conn.getRealConnection().rollback();
        }
        
        conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword()));
        // 记录checkout时间
        conn.setCheckoutTimestamp(System.currentTimeMillis());
        conn.setLastUsedTimestamp(System.currentTimeMillis());
        state.activeConnections.add(conn);
        
        // 请求个数
        state.requestCount++;
        // 请求时间
        state.accumulatedRequestTime += System.currentTimeMillis() - t;
        
      } else {
        logger.info("A bad connection ({}) was returned from the pool, getting another connection.", conn.getRealHashCode());
        // 如果没拿到，统计信息：失败链接 +1
        state.badConnectionCount++;
        localBadConnectionCount++;
        conn = null;
        
        // 失败次数较多，抛异常
        if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
          logger.debug("PooledDataSource: Could not get a good connection to the database.");
          throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
        }
      }
    }
    
    if (null == conn) {
      logger.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }
    
    return conn;
  }
  
  /**
   * 强制 关闭连接的方式
   *
   * @author lihh
   */
  public void forceCloseAll() {
    synchronized (state) {
      // 拿到 通过url拼接 的hashCode
      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
      
      for (int idx = state.activeConnections.size() - 1; idx >= 0; idx -= 1) {
        try {
          PooledConnection conn = state.activeConnections.remove(idx);
          // 将连接 重置为无效的
          conn.invalidate();
          
          Connection realConn = conn.getRealConnection();
          // 回滚是为了 保证数据库中数据的一致性
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
      
      // 关闭空闲的连接
      for (int idx = state.idleConnections.size() - 1; idx >= 0; idx -= 1) {
        try {
          PooledConnection conn = state.idleConnections.remove(idx);
          conn.invalidate();
          
          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) {
            realConn.rollback();
          }
          
          // 从这里关闭 真实的 connection
          realConn.close();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
      
      logger.info("PooledDataSource forcefully closed/removed all connections.");
    }
  }
  
  /**
   * ping 连接的处理，判断connection 是否为可用状态
   *
   * @param conn 表示连接
   * @return 返回 connection 是否为成功状态
   * @author lih
   */
  protected boolean pingConnection(PooledConnection conn) {
    boolean result = false;
    
    try {
      result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {
      logger.info("Connection {} is BAD: {}", conn.getRealHashCode(), e.getMessage());
    }
    
    if (result) {
      // 这里判断 是否开启 ping 模式
      if (poolPingEnabled) {
        // poolPingConnectionsNotUsedFor 表示 ping 的时间间隔
        // 这里【conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor】 判断时间间隔 是否超过指定的时间
        if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
          
          try {
            logger.info("Testing connection {} ...", conn.getRealHashCode());
            Connection realConn = conn.getRealConnection();
            
            Statement statement = realConn.createStatement();
            // 这里尝试执行 ping语句
            ResultSet resultSet = statement.executeQuery(poolPingQuery);
            // 关闭 resultSet
            resultSet.close();
            // 如果不是自动提交 就直接回滚
            if (!realConn.getAutoCommit()) {
              realConn.rollback();
            }
            logger.info("Connection {} is GOOD!", conn.getRealHashCode());
            
          } catch (Exception e) {
            logger.info("Execution of ping query '{}' failed: {}", poolPingQuery, e.getMessage());
            try {
              conn.getRealConnection().close();
            } catch (SQLException ignore) {
            }
            logger.info("Connection {} is BAD: {}", conn.getRealHashCode(), e.getMessage());
          }
          
        }
      }
    }
    
    return result;
  }
  
  public static Connection unwrapConnection(Connection conn) {
    // 判断 是否为代理类
    if (Proxy.isProxyClass(conn.getClass())) {
      // 拿到代理实现的类
      InvocationHandler handler = Proxy.getInvocationHandler(conn);
      
      // 判断实现的类
      if (handler instanceof PooledConnection) {
        // 拿到 真是的连接
        return ((PooledConnection) handler).getRealConnection();
      }
    }
    return conn;
  }
  
  /**
   * 根据 url，username 以及password 构建一个hashcode
   *
   * @param url      db url
   * @param username db username
   * @param password db password
   * @return 返回 hash code
   * @author lihh
   */
  private int assembleConnectionTypeCode(String url, String username, String password) {
    return (url + username + password).hashCode();
  }
  
  /**
   * 此时的get 并不仅仅是创建那么简单了，而是先从连接池中判断，情非得已下 从连接池中拿
   *
   * @return 返回连接 Connection
   * @throws SQLException 异常
   * @author lihh
   */
  @Override
  public Connection getConnection() throws SQLException {
    return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  }
  
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return popConnection(username, password).getProxyConnection();
  }
  
  @Override
  public <T> T unwrap(Class<T> iface) throws SQLException {
    throw new SQLException(getClass().getName() + " is not a wrapper.");
  }
  
  @Override
  public boolean isWrapperFor(Class<?> iface) throws SQLException {
    return false;
  }
  
  @Override
  public PrintWriter getLogWriter() throws SQLException {
    return DriverManager.getLogWriter();
  }
  
  @Override
  public void setLogWriter(PrintWriter logWriter) throws SQLException {
    DriverManager.setLogWriter(logWriter);
  }
  
  @Override
  public void setLoginTimeout(int loginTimeout) throws SQLException {
    DriverManager.setLoginTimeout(loginTimeout);
  }
  
  @Override
  public int getLoginTimeout() throws SQLException {
    return DriverManager.getLoginTimeout();
  }
  
  @Override
  public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
  }
  
  public void setDriver(String driver) {
    dataSource.setDriver(driver);
    forceCloseAll();
  }
  
  public void setUrl(String url) {
    dataSource.setUrl(url);
    forceCloseAll();
  }
  
  public void setUsername(String username) {
    dataSource.setUsername(username);
    forceCloseAll();
  }
  
  public void setPassword(String password) {
    dataSource.setPassword(password);
    forceCloseAll();
  }
  
  
  public void setDefaultAutoCommit(boolean defaultAutoCommit) {
    dataSource.setAutoCommit(defaultAutoCommit);
    forceCloseAll();
  }
  
  public int getPoolMaximumActiveConnections() {
    return poolMaximumActiveConnections;
  }
  
  public void setPoolMaximumActiveConnections(int poolMaximumActiveConnections) {
    this.poolMaximumActiveConnections = poolMaximumActiveConnections;
  }
  
  public int getPoolMaximumIdleConnections() {
    return poolMaximumIdleConnections;
  }
  
  public void setPoolMaximumIdleConnections(int poolMaximumIdleConnections) {
    this.poolMaximumIdleConnections = poolMaximumIdleConnections;
  }
  
  public int getPoolMaximumCheckoutTime() {
    return poolMaximumCheckoutTime;
  }
  
  public void setPoolMaximumCheckoutTime(int poolMaximumCheckoutTime) {
    this.poolMaximumCheckoutTime = poolMaximumCheckoutTime;
  }
  
  public int getPoolTimeToWait() {
    return poolTimeToWait;
  }
  
  public void setPoolTimeToWait(int poolTimeToWait) {
    this.poolTimeToWait = poolTimeToWait;
  }
  
  public String getPoolPingQuery() {
    return poolPingQuery;
  }
  
  public void setPoolPingQuery(String poolPingQuery) {
    this.poolPingQuery = poolPingQuery;
  }
  
  public boolean isPoolPingEnabled() {
    return poolPingEnabled;
  }
  
  public void setPoolPingEnabled(boolean poolPingEnabled) {
    this.poolPingEnabled = poolPingEnabled;
  }
  
  public int getPoolPingConnectionsNotUsedFor() {
    return poolPingConnectionsNotUsedFor;
  }
  
  public void setPoolPingConnectionsNotUsedFor(int poolPingConnectionsNotUsedFor) {
    this.poolPingConnectionsNotUsedFor = poolPingConnectionsNotUsedFor;
  }
  
  public int getExpectedConnectionTypeCode() {
    return expectedConnectionTypeCode;
  }
  
  public void setExpectedConnectionTypeCode(int expectedConnectionTypeCode) {
    this.expectedConnectionTypeCode = expectedConnectionTypeCode;
  }
}
